<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go 应用代码解析 - 核心业务逻辑</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>6. 核心业务逻辑 (Service Layer)</h1>
        <nav>
            <a href="index.html">概述</a> |
            <a href="01_setup_dependencies.html">1. 依赖与全局变量</a> |
            <a href="02_opentelemetry_init.html">2. OpenTelemetry 初始化</a> |
            <a href="03_database_redis_init.html">3. 数据库与 Redis 初始化</a> |
            <a href="04_web_server_routing.html">4. Web 服务器与路由</a> |
            <a href="05_request_handlers.html">5. HTTP 请求处理</a> |
            <a href="06_business_logic.html" class="active">6. 核心业务逻辑</a> |
            <a href="07_main_startup_shutdown.html">7. 程序启动与关闭</a>
        </nav>
    </header>

    <main>
        <p>业务逻辑层（有时也叫 Service 层）是应用程序的核心，它封装了具体的业务规则和数据操作流程。在我们的例子中，它负责与数据库 (MySQL) 和缓存 (Redis) 直接交互。</p>
        <p>这一层的函数同样需要创建自己的 OTel Span，继承自调用它的 Handler 层 Span，从而形成更详细的追踪链。</p>

        <section id="find-user">
            <h2>查找用户：`findUserByID` (缓存优先策略)</h2>
            <p>这个函数实现了根据用户 ID 查找用户的逻辑。它采用了一个常见的性能优化策略：**缓存优先 (Cache-Aside)**。</p>

             <h3>缓存优先 (Cache-Aside) 策略流程图</h3>
             <div class="diagram-container">
                <object type="image/svg+xml" data="diagrams/cache_aside_flow.svg" class="diagram">
                    你的浏览器不支持 SVG，无法显示缓存优先流程图。
                </object>
             </div>

             <pre><code class="language-go">// findUserByID 业务逻辑：根据 ID 查找用户（缓存优先）。
func findUserByID(ctx context.Context, id string) (*User, error) {
	// 1. 开始一个新的 OTel Span (作为 Handler Span 的子 Span)
	var bizSpan trace.Span
	// 同样从 ctx 获取父 Span 信息
	ctx, bizSpan = tracer.Start(ctx, "Service: FindUserByID",
		trace.WithAttributes(attribute.String("app.user.id", id)), // 记录要查的 ID
		trace.WithSpanKind(trace.SpanKindInternal), // 仍然是内部操作
	)
	defer bizSpan.End() // 确保 Span 结束

	var user User
	var cacheHit bool = false // 标记是否命中缓存

	// 2. 先尝试从 Redis 缓存获取数据
	cacheKey := fmt.Sprintf("user:%s", id) // 构造缓存 Key (例如 "user:123")
	// 在 Span 中添加事件，记录要检查的 Key
	bizSpan.AddEvent("检查 Redis 缓存", trace.WithAttributes(attribute.String("cache.key", cacheKey)))

	// 调用 Redis GET 命令。
	// 因为我们初始化 rdb 时集成了 redisotel 插件，所以这次 Redis 调用会自动创建一个子 Span！
	jsonData, err := rdb.Get(ctx, cacheKey).Result()

	// 3. 处理 Redis GET 的结果
	if err == nil { // 如果 err 是 nil，说明 Redis 中存在这个 Key
		cacheHit = true
		bizSpan.AddEvent("缓存命中")
		bizSpan.SetAttributes(attribute.Bool("cache.hit", true)) // 在 Span 上标记缓存命中

		// 尝试将从 Redis 取出的 JSON 字符串反序列化回 User 结构体
		if errUnmarshal := json.Unmarshal([]byte(jsonData), &user); errUnmarshal != nil {
			// 如果反序列化失败 (说明缓存数据可能已损坏)
			bizSpan.RecordError(errUnmarshal, trace.WithAttributes(attribute.String("app.error.type", "CacheUnmarshalFailed")))
			bizSpan.SetStatus(codes.Error, "缓存数据反序列化失败")
			log.Printf("错误: 反序列化用户 '%s' 的缓存数据失败: %v", id, errUnmarshal)
			bizSpan.AddEvent("缓存反序列化失败，继续查询数据库")
			cacheHit = false // 标记为未命中，强制去查数据库
		} else {
			// 如果成功反序列化
			bizSpan.SetStatus(codes.Ok, "用户在缓存中找到")
			return &user, nil // 直接返回从缓存中获取的用户数据，流程结束
		}
	} else if !errors.Is(err, redis.Nil) { // 如果发生了错误，但不是 "key不存在" (redis.Nil) 错误
		// 可能是 Redis 连接问题或其他 Redis 错误
		bizSpan.RecordError(err, trace.WithAttributes(attribute.String("app.error.type", "RedisError")))
		bizSpan.SetStatus(codes.Error, "Redis 缓存检查失败")
		log.Printf("警告: 检查用户 '%s' 的缓存时发生 Redis 错误: %v", id, err)
		// 虽然 Redis 出错，但我们选择继续尝试查询数据库（容错）
		bizSpan.AddEvent("Redis Get 操作出错，继续查询数据库")
	} else { // 如果 err 是 redis.Nil，说明缓存中没有这个 Key
		bizSpan.AddEvent("缓存未命中")
		bizSpan.SetAttributes(attribute.Bool("cache.hit", false)) // 标记未命中
	}

	// 4. 如果缓存未命中 (cacheHit 为 false)，则查询数据库
	if !cacheHit {
		bizSpan.AddEvent("查询数据库")
		// 使用全局的 db 对象执行查询。
		// db.WithContext(ctx) 是关键，它将带有 OTel信息的 ctx 传递给 GORM。
		// 因为我们初始化 db 时集成了 otelgorm 插件，所以这次数据库调用也会自动创建一个子 Span！
		result := db.WithContext(ctx).First(&user, "id = ?", id) // SELECT * FROM users WHERE id = ? LIMIT 1

		// 5. 处理数据库查询结果
		if result.Error != nil { // 如果查询出错
			bizSpan.RecordError(result.Error) // 在 Span 记录错误
			if errors.Is(result.Error, gorm.ErrRecordNotFound) { // 如果是 "未找到记录" 错误
				bizSpan.SetAttributes(attribute.Bool("db.found", false)) // 标记数据库未找到
				bizSpan.SetStatus(codes.Error, "用户在数据库中未找到") // 设置 Span 状态
			} else { // 其他数据库错误 (连接失败、SQL语法错误等)
				bizSpan.SetAttributes(attribute.String("app.error.type", "DatabaseError"))
				bizSpan.SetStatus(codes.Error, "数据库查询失败")
			}
			return nil, result.Error // 将数据库错误返回给上层 (Handler)
		}
		// 如果数据库查询成功
		bizSpan.SetAttributes(attribute.Bool("db.found", true)) // 标记数据库找到
		bizSpan.AddEvent("数据库查询成功")

		// 6. 将从数据库查到的数据写入 Redis 缓存，供下次使用
		bizSpan.AddEvent("更新 Redis 缓存")
		// 将 User 结构体序列化为 JSON 字符串
		jsonDataBytes, errMarshal := json.Marshal(&user)
		if errMarshal != nil { // 如果序列化失败
			bizSpan.RecordError(errMarshal, trace.WithAttributes(attribute.String("app.error.type", "CacheMarshalFailed")))
			bizSpan.AddEvent("警告: 用户数据序列化失败，无法更新缓存")
			log.Printf("警告: 序列化用户 '%s' 数据以便写入缓存失败: %v", id, errMarshal)
			// 虽然缓存写入失败，但用户数据已从数据库查到，仍然成功返回
			bizSpan.SetStatus(codes.Ok, "用户在数据库中找到，但缓存更新失败 (序列化错误)")
			return &user, nil // 返回从数据库获取的用户
		}

		// 设置一个随机的缓存过期时间 (5-10分钟)，防止缓存雪崩
		cacheTTL := time.Duration(5+rand.Intn(6)) * time.Minute
		bizSpan.AddEvent("设置缓存", trace.WithAttributes(
			attribute.String("cache.key", cacheKey),
			attribute.String("cache.ttl", cacheTTL.String()), // 记录TTL
		))
		// 执行 Redis SET 命令，将 JSON 数据存入 Redis，并设置过期时间
		// 这个 Redis 调用同样会被 redisotel 自动追踪
		setCmd := rdb.Set(ctx, cacheKey, string(jsonDataBytes), cacheTTL)
		if errSet := setCmd.Err(); errSet != nil { // 如果 SET 命令执行失败
			bizSpan.RecordError(errSet, trace.WithAttributes(attribute.String("app.error.type", "RedisError")))
			bizSpan.AddEvent("警告: Redis 缓存更新失败")
			log.Printf("警告: 更新用户 '%s' 的 Redis 缓存失败: %v", id, errSet)
			// 同样，缓存更新失败不影响本次查询结果
			bizSpan.SetStatus(codes.Ok, "用户在数据库中找到，但缓存更新失败 (Redis Set 错误)")
			return &user, nil // 返回从数据库获取的用户
		}

		// 如果缓存更新成功
		bizSpan.AddEvent("缓存更新成功")
		bizSpan.SetAttributes(attribute.Bool("cache.updated", true)) // 标记缓存已更新
		bizSpan.SetStatus(codes.Ok, "用户在数据库中找到并已更新缓存")
		return &user, nil // 返回从数据库获取的用户
	}

	// 这段代码理论上不应该执行到，除非逻辑有误
	log.Println("警告: findUserByID 执行流程异常结束")
	bizSpan.SetStatus(codes.Error, "内部逻辑错误")
	return nil, errors.New("findUserByID 内部逻辑错误")
}
</code></pre>
            <p><strong>核心逻辑 (Cache-Aside):</strong></p>
            <ol>
                <li><strong>查缓存 (Redis):</strong> 尝试从 Redis 获取数据。
                    <ul>
                        <li><strong>命中且有效:</strong> 直接返回缓存数据，结束。OTel Span 标记 cache.hit=true。</li>
                        <li><strong>命中但无效 (反序列化失败):</strong> 记录错误，标记为未命中，继续查数据库。</li>
                        <li><strong>未命中 (Key 不存在):</strong> 继续查数据库。OTel Span 标记 cache.hit=false。</li>
                        <li><strong>Redis 出错 (非 Key 不存在):</strong> 记录错误，但为了容错，继续查数据库。</li>
                    </ul>
                </li>
                <li><strong>查数据库 (MySQL via GORM):</strong> 仅在缓存未命中或无效时执行。
                    <ul>
                        <li><strong>找到:</strong> 记录成功，OTel Span 标记 db.found=true。继续步骤 3。</li>
                        <li><strong>未找到:</strong> 记录错误 (RecordNotFound)，设置 OTel Span 状态为 Error，返回错误给上层。</li>
                        <li><strong>数据库出错:</strong> 记录错误，设置 OTel Span 状态为 Error，返回错误给上层。</li>
                    </ul>
                </li>
                 <li><strong>写缓存 (Redis):</strong> 在数据库成功查到数据后，将数据序列化为 JSON 并写入 Redis，同时设置一个过期时间 (TTL)。
                    <ul>
                        <li><strong>写入成功:</strong> 记录成功，OTel Span 标记 cache.updated=true。返回从数据库查到的数据给上层。</li>
                        <li><strong>写入失败 (序列化或 Redis Set 错误):</strong> 记录警告和 OTel 错误，但不影响本次请求的成功，仍然返回从数据库查到的数据给上层。</li>
                    </ul>
                 </li>
                 <li><strong>OTel 集成体现：</strong>注意函数开头创建了 "Service: FindUserByID" Span，并且在调用 `rdb.Get`, `db.WithContext(ctx).First`, `rdb.Set` 时，由于 `redisotel` 和 `otelgorm` 插件的存在，会自动创建相应的子 Span，详细记录这些 I/O 操作的耗时和信息。这使得在追踪系统中可以清晰地看到是缓存命中、数据库查询还是缓存写入花费了主要时间。</li>
            </ol>
        </section>

        <section id="save-user">
            <h2>保存用户：`saveUser` (数据库优先，缓存失效)</h2>
            <p>这个函数负责将新的用户信息保存到数据库，并处理相关的缓存。</p>
            <p>对于写操作（创建、更新、删除），一种常见的缓存策略是 **缓存失效 (Cache Invalidation)**：修改数据库后，直接删除缓存中对应的旧数据，而不是去更新缓存。这样下次读取时，就会因为缓存未命中而去数据库加载最新数据，并重新写入缓存。这种方式逻辑相对简单。</p>

            <pre><code class="language-go">// saveUser 业务逻辑：保存新用户到数据库，并使相关缓存失效。
func saveUser(ctx context.Context, user *User) (*User, error) {
	// 1. 开始一个新的 OTel Span
	var bizSpan trace.Span
	ctx, bizSpan = tracer.Start(ctx, "Service: SaveUser",
		trace.WithAttributes(
			attribute.String("app.user.id", user.ID),
			attribute.String("app.user.email", user.Email), // 注意：实际生产中可能需要脱敏 Email
		),
		trace.WithSpanKind(trace.SpanKindInternal),
	)
	defer bizSpan.End() // 确保结束

	var dbErrorOccurred bool = false
	var cacheErrorOccurred bool = false
	var dbErr, cacheErr error // 用于保存可能发生的错误

	// 2. 将用户数据写入数据库
	bizSpan.AddEvent("开始保存用户到数据库")
	// 使用 db.WithContext(ctx).Create 保存用户记录。
	// otelgorm 插件会自动追踪这次数据库 INSERT 操作。
	result := db.WithContext(ctx).Create(user)
	if result.Error != nil { // 如果数据库写入失败
		dbErrorOccurred = true
		dbErr = result.Error
		bizSpan.RecordError(dbErr, trace.WithAttributes(attribute.String("app.error.type", "DatabaseError")))
		bizSpan.SetStatus(codes.Error, "保存用户到数据库失败")
		log.Printf("错误: 保存用户 (ID: %s) 到数据库失败: %v", user.ID, dbErr)
		// 直接返回错误给上层 Handler
		return nil, fmt.Errorf("数据库创建用户失败: %w", dbErr)
	}
	// 如果数据库写入成功
	bizSpan.SetAttributes(attribute.Int64("db.rows_affected", result.RowsAffected)) // 记录影响的行数 (应为 1)
	bizSpan.AddEvent("用户成功保存到数据库")

	// 3. 使 Redis 缓存失效 (删除对应的 Key)
	bizSpan.AddEvent("使 Redis 缓存失效")
	cacheKey := fmt.Sprintf("user:%s", user.ID) // 构造和读取时一致的 Key
	bizSpan.SetAttributes(attribute.String("cache.key", cacheKey)) // 记录要删除的 Key

	// 执行 Redis DEL 命令。
	// redisotel 插件会自动追踪这次 Redis DEL 操作。
	delCmd := rdb.Del(ctx, cacheKey)
	deletedCount := delCmd.Val() // 获取实际删除的 Key 的数量 (可能是 0 或 1)
	bizSpan.SetAttributes(attribute.Int64("cache.invalidated_count", deletedCount))

	// 检查 DEL 命令本身是否出错 (比如 Redis 连接问题)
	// 我们忽略 redis.Nil 错误，因为 Key 本来就可能不存在缓存中
	if err := delCmd.Err(); err != nil && !errors.Is(err, redis.Nil) {
		cacheErrorOccurred = true
		cacheErr = err
		bizSpan.RecordError(cacheErr, trace.WithAttributes(attribute.String("app.error.type", "RedisError")))
		bizSpan.AddEvent("警告: Redis 缓存失效失败")
		log.Printf("警告: 失效用户 '%s' 的 Redis 缓存失败: %v", user.ID, cacheErr)
		// 注意：即使缓存失效失败，我们仍然认为用户创建成功了，因为主数据已入库。
		// 只是后续的第一次读取可能会读到旧的缓存（如果存在的话），直到缓存过期。
	} else {
		// DEL 命令执行成功（可能删除了 0 或 1 个 key）
		if deletedCount > 0 {
			bizSpan.AddEvent("缓存成功失效")
		} else {
			bizSpan.AddEvent("缓存键不存在，无需失效")
		}
	}

	// 4. 根据操作结果设置最终的 OTel Span 状态
	// (如果 dbErrorOccurred 为 true, 前面已经 return 了，不会执行到这里)
	if cacheErrorOccurred {
		// 数据库成功，但缓存失效失败
		bizSpan.SetStatus(codes.Ok, "用户已保存到数据库，但缓存失效失败")
		// 也可以选择设置为 codes.Error，取决于你对缓存一致性的要求严格程度
	} else {
		// 数据库成功，缓存处理也成功（或无需处理）
		bizSpan.SetStatus(codes.Ok, "用户成功保存，缓存已处理")
	}

	// 5. 返回创建成功的用户对象 (可能包含了 GORM 自动填充的 CreateAt/UpdatedAt 时间)
	return user, nil
}
</code></pre>
             <p><strong>核心逻辑 (写操作与缓存失效):</strong></p>
            <ol>
                <li><strong>启动 OTel Span:</strong> 创建 "Service: SaveUser" Span。</li>
                <li><strong>写数据库:</strong> 使用 `db.WithContext(ctx).Create(user)` 将用户信息插入到 MySQL 数据库的 `users` 表中。
                    <ul>
                        <li><strong>成功:</strong> 记录影响行数，继续执行。</li>
                        <li><strong>失败:</strong> 记录 OTel 错误，设置 Span 状态为 Error，将错误返回给上层 Handler。</li>
                    </ul>
                </li>
                <li><strong>删缓存:</strong> 构造对应的 Redis Key (`user:ID`)，然后执行 `rdb.Del(ctx, cacheKey)` 命令尝试删除这个 Key。
                    <ul>
                        <li><strong>DEL 命令成功 (无论是否真的删除了 Key):</strong> 记录 OTel 事件表明缓存已处理。</li>
                        <li><strong>DEL 命令失败 (非 Key 不存在错误):</strong> 记录 OTel 警告和错误，标记 `cacheErrorOccurred`。但<strong>不</strong>中断流程或返回错误，因为主数据已成功写入数据库。</li>
                    </ul>
                </li>
                <li><strong>设置最终 Span 状态:</strong> 根据数据库和缓存操作的结果，设置 Span 的最终状态 (通常是 OK，除非数据库写入失败)。</li>
                <li><strong>返回结果:</strong> 返回成功保存的 `user` 对象（可能包含 GORM 自动填充的字段）和 `nil` 错误给上层 Handler。</li>
                <li><strong>OTel 集成体现：</strong>同样地，`db.Create` 和 `rdb.Del` 操作会自动被 `otelgorm` 和 `redisotel` 插件追踪，生成子 Span，记录操作详情。</li>
            </ol>
        </section>

        <section id="navigation">
            <h2>下一步</h2>
            <p>我们已经深入了解了处理请求的 Handler 和执行核心任务的 Service 层。最后，让我们回到程序的起点和终点：`main` 函数，看看它是如何把所有这些部件组装起来运行，以及如何优雅地停止服务的。</p>
            <p>➡️ <a href="07_main_startup_shutdown.html">点击这里，查看程序的启动与关闭流程</a></p>
        </section>
    </main>

    <footer>
        <p>Go 应用代码解析</p>
    </footer>
</body>
</html>